package com.eyealike.client.rest;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.eyealike.client.EmptyResponseQueueException;
import com.eyealike.client.EyealikeAPI;
import com.eyealike.client.EyealikePlatformException;
import com.eyealike.client.ServiceMapping;
import com.eyealike.client.util.ArrayUtils;
import com.eyealike.client.util.SecurityUtils;
import com.eyealike.vsp.bindings.BatchIndexRequest;
import com.eyealike.vsp.bindings.BatchIndexResponse;
import com.eyealike.vsp.bindings.ClassificationRequest;
import com.eyealike.vsp.bindings.ClassificationResponse;
import com.eyealike.vsp.bindings.ComparisonResponse;
import com.eyealike.vsp.bindings.ErrorResponse;
import com.eyealike.vsp.bindings.ImageCollection;
import com.eyealike.vsp.bindings.IndexRequest;
import com.eyealike.vsp.bindings.IndexResponse;
import com.eyealike.vsp.bindings.ProductSimilarityRequest;
import com.eyealike.vsp.bindings.ProductSimilarityResponse;
import com.eyealike.vsp.bindings.ProductSimilarityResult;
import com.eyealike.vsp.bindings.QueueStatusResponse;
import com.eyealike.vsp.bindings.SearchRequest;
import com.eyealike.vsp.bindings.SearchResponse;
import com.eyealike.vsp.bindings.BatchIndexRequest.Image;
import com.eyealike.vsp.bindings.ProductSimilarityRequest.ProductImage;
import com.eyealike.vsp.bindings.SearchRequest.QueryImage;

/**
 * A thread-safe client to Eyealike's visual search platform. 
 */

public class RestEyealikeClient implements EyealikeAPI
{

	private final String hostName;
	private final boolean useSslConnections;
	private final String accessId;
	private final String secretKey;

	protected final HttpClient client;
	protected final JAXBContext context;
	private final MultiThreadedHttpConnectionManager connectionManager;

	protected RestEyealikeClient(
		final String bindingPackages,
		final boolean useSslConnections,
		final String hostName,
		final String accessId,
		final String secretKey )
	{
		this.accessId = accessId;
		this.secretKey = secretKey;
		this.useSslConnections = useSslConnections;
		this.hostName = hostName;
		connectionManager = new MultiThreadedHttpConnectionManager();
		client = new HttpClient( connectionManager );
		try {
			context = JAXBContext.newInstance( bindingPackages );
		} catch( final JAXBException e ) {
			throw new RuntimeException( e );
		}
	}

	protected static final String BINDINGS_PACKAGE = "com.eyealike.vsp.bindings";

	public RestEyealikeClient(
		final boolean useSslConnections,
		final String hostName,
		final String accessId,
		final String secretKey )
	{
		this( BINDINGS_PACKAGE, useSslConnections, hostName, accessId, secretKey );
	}

	public RestEyealikeClient( final String hostName, final String accessId, final String secretKey )
	{
		this( false, hostName, accessId, secretKey );
	}

	public RestEyealikeClient( final String accessId, final String secretKey )
	{
		this( false, "self.eyealike.com", accessId, secretKey );
	}

	public void setSetMaxConnections( final int maxConnections) {
		connectionManager.getParams().setDefaultMaxConnectionsPerHost( maxConnections );
		connectionManager.getParams().setMaxTotalConnections( maxConnections );
	}
	
	protected String getHostName()
	{
		return hostName;
	}

	protected String assembleEndpointUrl( final String hostName, final String endpoint )
	{
		return ( useSslConnections ? "https" : "http" ) + "://" + hostName + "/vsp/" + endpoint;
	}

	protected String assembleEndpointUrl( final String hostName, final ServiceMapping def )
	{
		return assembleEndpointUrl( hostName, def.getEndpoint() );
	}

	
	
	/**
	 * Used to support test utility. We might need to refactor this a bit.
	 */
	public String createSecurityHeader(
		final String targetUrl,
		final String requestMethod,
		final String serviceId,
		final Document xmlDoc,
		final String dateHeader )
	{
		final StringBuilder signedMessage = new StringBuilder();
		signedMessage.append( accessId );
		signedMessage.append( "\n" );
		signedMessage.append( serviceId );
		signedMessage.append( "\n" );

		if( requestMethod.equals( "DELETE" ) || requestMethod.equals( "GET" ) ) {
			final String uri = StringUtils.split( targetUrl, "?" )[ 0 ];
			final String[] uriTokens = StringUtils.split( uri, "/" );
			if( uriTokens.length == 5 ) {
				signedMessage.append( uriTokens[ 4 ] );
				signedMessage.append( "\n" );
			}
		} else {
			NodeList nodes = xmlDoc.getElementsByTagName( "url" );
			Node node;
			for( int i = 0; i < nodes.getLength(); i++ ) {
				node = nodes.item( i );
				signedMessage.append( node.getFirstChild().getNodeValue() );
				signedMessage.append( "\n" );
			}

			nodes = xmlDoc.getElementsByTagName( "roi-id" );
			for( int i = 0; i < nodes.getLength(); i++ ) {
				node = nodes.item( i );
				signedMessage.append( node.getFirstChild().getNodeValue() );
				signedMessage.append( "\n" );
			}

			nodes = xmlDoc.getElementsByTagName( "collection-id" );
			for( int i = 0; i < nodes.getLength(); i++ ) {
				node = nodes.item( i );
				signedMessage.append( node.getFirstChild().getNodeValue() );
				signedMessage.append( "\n" );
			}

			nodes = xmlDoc.getElementsByTagName( "classifier-code" );
			for( int i = 0; i < nodes.getLength(); i++ ) {
				node = nodes.item( i );
				signedMessage.append( node.getFirstChild().getNodeValue() );
				signedMessage.append( "\n" );
			}
		}
		signedMessage.append( dateHeader.trim() );

		final String digest = SecurityUtils.computeDigest( signedMessage.toString(), secretKey );

		return "EYEALIKE " + accessId + " " + digest;
	}

	protected void populateSecurityHeaders(
		final HttpMethod method,
		final String serviceId,
		final List<String> queryUrls,
		final List<String> roiIds,
		final List<String> collectionIds,
		final List<String> classifierCodes )
	{
		final String requestDate = SecurityUtils.getUTCString();
		method.addRequestHeader( "Date", requestDate );
		method.addRequestHeader( "Authorization", SecurityUtils.createAuthorizationHeader(
			accessId,
			secretKey,
			serviceId,
			queryUrls,
			roiIds,
			collectionIds,
			classifierCodes,
			requestDate ) );
	}

	// TODO we should consider a resource pool here
	private Unmarshaller getUnmarshaller() throws JAXBException
	{
		return context.createUnmarshaller();
	}

	// TODO we should consider a resource pool here
	private Marshaller getMarshaller() throws JAXBException
	{
		return context.createMarshaller();
	}

	protected void populateSecurityHeaders(
		final HttpMethod method,
		final ServiceMapping service,
		final List<String> queryUrls,
		final List<String> roiIds,
		final List<String> collectionIds,
		final List<String> classifierCodes )
	{
		populateSecurityHeaders( method, service.getServiceId(), queryUrls, roiIds, collectionIds, classifierCodes );
	}

	protected void populateSecurityHeaders( final HttpMethod method, final ServiceMapping service )
	{
		populateSecurityHeaders( method, service, new String[] {}, new String[] {}, new String[] {}, new String[] {} );
	}

	protected void populateSecurityHeaders(
		final HttpMethod method,
		final ServiceMapping service,
		final String[] queryUrls,
		final String[] roiIds,
		final String[] collectionIds,
		final String[] classifierCodes )
	{
		populateSecurityHeaders(
			method,
			service.getServiceId(),
			ArrayUtils.toList( queryUrls ),
			ArrayUtils.toList( roiIds ),
			ArrayUtils.toList( collectionIds ),
			ArrayUtils.toList( classifierCodes ) );
	}

	protected boolean executeBooleanMethod( final HttpMethod method )
	{
		try {
			client.executeMethod( method );
			// add content type based error response check here
			final String response = new String( method.getResponseBody() );

			return "OK".equals( response.trim() );
		} catch( final HttpException e ) {
			throw new RuntimeException( e );
		} catch( final IOException e ) {
			throw new RuntimeException( e );
		}
	}

	protected Object executeMethod( final HttpMethod method ) throws EyealikePlatformException
	{
		try {
			client.executeMethod( method );
			if( method.getStatusCode() != 200 ) {
				System.out.println( method.getResponseBodyAsString() );
				throw new HttpException( method.getStatusText() );
			}

			final Object result = unmarshall( method.getResponseBodyAsStream() );
			if( result instanceof ErrorResponse ) {
				throw new EyealikePlatformException( (ErrorResponse) result );
			} else {
				return result;
			}
		} catch( final HttpException e ) {
			throw new RuntimeException( e );
		} catch( final IOException e ) {
			throw new RuntimeException( e );
		}
	}

	public Object unmarshall( final String xml )
	{
		return unmarshall( new ByteArrayInputStream( xml.getBytes() ) );
	}

	public Object unmarshall( final InputStream in )
	{
		try {
			return getUnmarshaller().unmarshal( in );
		} catch( final JAXBException e ) {
			throw new RuntimeException( e );
		}
	}

	protected void populatePayload( final PutMethod method, final Object payloadObject )
	{
		final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		try {
			getMarshaller().marshal( payloadObject, outStream );

			method.setRequestEntity( new ByteArrayRequestEntity( outStream.toByteArray() ) );
		} catch( final JAXBException e ) {
			throw new RuntimeException( e );
		}
	}

	protected void populatePayload( final PostMethod method, final Object payloadObject )
	{
		final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		try {
			getMarshaller().marshal( payloadObject, outStream );

			method.setRequestEntity( new ByteArrayRequestEntity( outStream.toByteArray() ) );
		} catch( final JAXBException e ) {
			throw new RuntimeException( e );
		}
	}

	@Override
	public boolean unindex( final String imageUrl )
	{
		try {
			final DeleteMethod method =
				new DeleteMethod( assembleEndpointUrl( hostName, ServiceMapping.UNINDEX ) + "/" + URLEncoder.encode(
				imageUrl,
				"UTF-8" ) );
			populateSecurityHeaders( method, ServiceMapping.UNINDEX, new String[] { imageUrl }, null, null, null );
			return executeBooleanMethod( method );
		} catch( final UnsupportedEncodingException e ) {
			throw new RuntimeException( e );
		}
	}

	@Override
	public ClassificationResponse classifyRealTime( final ClassificationRequest request )
		throws EyealikePlatformException
	{
		final PostMethod method = new PostMethod( assembleEndpointUrl( hostName, ServiceMapping.CLASSIFY ) );

		final List<String> roiIds = new ArrayList<String>();
		roiIds.add( request.getTargetId() );

		populateSecurityHeaders( method, ServiceMapping.CLASSIFY, null, roiIds, null, request.getClassifierCode() );
		populatePayload( method, request );
		return (ClassificationResponse) executeMethod( method );
	}

	@Override
	public ImageCollection createOrUpdateCollection( final ImageCollection collection )
		throws EyealikePlatformException
	{
		final PutMethod method = new PutMethod( assembleEndpointUrl( hostName, ServiceMapping.COLLECTION_PUT ) );

		final List<String> cIds = new ArrayList<String>();
		if( collection.getCollectionId() != null ) {
			cIds.add( collection.getCollectionId() );
		}

		populateSecurityHeaders( method, ServiceMapping.COLLECTION_PUT, null, null, cIds, null );
		populatePayload( method, collection );
		return (ImageCollection) executeMethod( method );
	}

	@Override
	public ComparisonResponse compare( final String roiA, final String roiB, final String profileCode )
		throws EyealikePlatformException
	{
		final GetMethod method = new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.COMPARE ) );

		method.setQueryString( new NameValuePair[] {
			new NameValuePair( "roiA", roiA ),
			new NameValuePair( "roiB", roiB ),
			new NameValuePair( "profileCode", profileCode ) } );

		final List<String> rois = new ArrayList<String>();
		rois.add( roiA );
		rois.add( roiB );

		populateSecurityHeaders( method, ServiceMapping.COMPARE, rois, null, null, null );

		return (ComparisonResponse) executeMethod( method );
	}

	@Override
	public boolean isCached( final String id )
	{
		final GetMethod method =
			new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.MEDIA_CACHE_READ ) + "/" + id );
		method.setQueryString( new NameValuePair[] { new NameValuePair( "cacheCheck", "true" ) } );
		populateSecurityHeaders( method, ServiceMapping.MEDIA_CACHE_READ, null, new String[] { id }, null, null );

		try {
			client.executeMethod( method );
			final String response = new String( method.getResponseBody() );
			return Boolean.parseBoolean( response.trim() );
		} catch( final HttpException e ) {
			throw new RuntimeException( e );
		} catch( final IOException e ) {
			throw new RuntimeException( e );
		}
	}

	@Override
	public InputStream findCachedMedia( final String id )
	{
		final GetMethod method =
			new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.MEDIA_CACHE_READ ) + "/" + id );
		populateSecurityHeaders( method, ServiceMapping.MEDIA_CACHE_READ, null, new String[] { id }, null, null );

		try {
			client.executeMethod( method );

			return method.getResponseBodyAsStream();
		} catch( final HttpException e ) {
			throw new RuntimeException( e );
		} catch( final IOException e ) {
			throw new RuntimeException( e );
		}
	}

	@Override
	public boolean deleteCollection( final String collectionId )
	{
		final DeleteMethod method =
			new DeleteMethod( assembleEndpointUrl( hostName, ServiceMapping.COLLECTION_DELETE ) + "/" + collectionId );
		populateSecurityHeaders(
			method,
			ServiceMapping.COLLECTION_DELETE,
			null,
			null,
			new String[] { collectionId },
			null );
		return executeBooleanMethod( method );
	}

	@Override
	public QueueStatusResponse batchIndexQueueStatus() throws EyealikePlatformException
	{
		final GetMethod method =
			new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.BATCH_INDEX_RESULTS ) + "?operation=status" );

		populateSecurityHeaders( method, ServiceMapping.BATCH_INDEX_RESULTS );
		return (QueueStatusResponse) executeMethod( method );
	}

	@Override
	public BatchIndexResponse indexBatch( final BatchIndexRequest request ) throws EyealikePlatformException
	{
		final PutMethod method = new PutMethod( assembleEndpointUrl( hostName, ServiceMapping.BATCH_INDEX_SUBMIT ) );

		final List<String> urls = new ArrayList<String>();
		for( final Image img : request.getImage() ) {
			urls.add( img.getUrl() );
		}

		final List<String> cIds = new ArrayList<String>();
		cIds.add( request.getCollectionId() );

		populateSecurityHeaders(
			method,
			ServiceMapping.BATCH_INDEX_SUBMIT,
			urls,
			null,
			cIds,
			request.getClassifierCode() );
		populatePayload( method, request );
		return (BatchIndexResponse) executeMethod( method );
	}

	@Override
	public IndexResponse indexRealTime( final IndexRequest request ) throws EyealikePlatformException
	{
		final PostMethod method = new PostMethod( assembleEndpointUrl( hostName, ServiceMapping.REAL_TIME_INDEX ) );

		final List<String> urls = new ArrayList<String>();
		urls.add( request.getUrl() );

		populateSecurityHeaders(
			method,
			ServiceMapping.REAL_TIME_INDEX,
			urls,
			null,
			request.getCollectionId(),
			request.getClassifierCode() );
		populatePayload( method, request );
		return (IndexResponse) executeMethod( method );
	}

	@Override
	public IndexResponse nextBatchIndexResult() throws EyealikePlatformException, EmptyResponseQueueException
	{
		final GetMethod method = new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.BATCH_INDEX_RESULTS ) );

		populateSecurityHeaders( method, ServiceMapping.BATCH_INDEX_RESULTS );

		final Object response = executeMethod( method );
		if( response instanceof QueueStatusResponse ) {
			throw new EmptyResponseQueueException();
		}
		return (IndexResponse) response;
	}

	@Override
	public ProductSimilarityResult nextProductSimilarityResult()
		throws EyealikePlatformException,
		EmptyResponseQueueException
	{
		final GetMethod method =
			new GetMethod( assembleEndpointUrl( hostName, ServiceMapping.PRODUCT_SIMILARITY_RESULTS ) );

		populateSecurityHeaders( method, ServiceMapping.PRODUCT_SIMILARITY_RESULTS );

		final Object response = executeMethod( method );
		if( response instanceof QueueStatusResponse ) {
			throw new EmptyResponseQueueException();
		}
		return (ProductSimilarityResult) response;
	}

	@Override
	public QueueStatusResponse productSimilarityQueueStatus() throws EyealikePlatformException
	{
		final GetMethod method =
			new GetMethod(
			assembleEndpointUrl( hostName, ServiceMapping.PRODUCT_SIMILARITY_RESULTS ) + "?operation=status" );

		populateSecurityHeaders( method, ServiceMapping.PRODUCT_SIMILARITY_RESULTS );
		return (QueueStatusResponse) executeMethod( method );
	}

	@Override
	public ProductSimilarityResponse queueProductSimilarityJob( final ProductSimilarityRequest request )
		throws EyealikePlatformException
	{
		final PutMethod method =
			new PutMethod( assembleEndpointUrl( hostName, ServiceMapping.PRODUCT_SIMILARITY_SUBMIT ) );

		final List<String> urls = new ArrayList<String>();
		for( final ProductImage img : request.getProductImage() ) {
			urls.add( img.getUrl() );
		}

		final List<String> cIds = new ArrayList<String>();
		cIds.add( request.getCollectionId() );

		populateSecurityHeaders( method, ServiceMapping.PRODUCT_SIMILARITY_SUBMIT, urls, null, cIds, null );
		populatePayload( method, request );
		return (ProductSimilarityResponse) executeMethod( method );
	}

	@Override
	public SearchResponse search( final SearchRequest request ) throws EyealikePlatformException
	{
		final PostMethod method = new PostMethod( assembleEndpointUrl( hostName, ServiceMapping.REAL_TIME_SEARCH ) );

		final List<String> rois = new ArrayList<String>();
		for( final QueryImage img : request.getQueryImage() ) {
			rois.add( img.getRoiId() );
		}

		final List<String> cIds = new ArrayList<String>();
		cIds.add( request.getCollectionId() );

		populateSecurityHeaders( method, ServiceMapping.REAL_TIME_SEARCH, null, rois, cIds, null );
		populatePayload( method, request );
		return (SearchResponse) executeMethod( method );
	}
}
